;
; setup.s		(C) 1991 Linus Torvalds
;
; setup.s 负责从BIOS 中获取系统数据，并将这些数据放到系统内存的适当地方。
; 此时setup.s 和system 已经由bootsect 引导块加载到内存中。
; 
; 这段代码询问bios 有关内存/磁盘/其它参数，并将这些参数放到一个
;“安全的”地方：0x90000-0x901FF，也即原来bootsect 代码块曾经在
; 的地方，然后在被缓冲块覆盖掉之前由保护模式的system 读取。
;
; 注意：以下这些参数最好和bootsect.s 中的相同！
INITSEG  equ 0x9000	
SYSSEG   equ 0x1000	
SETUPSEG equ 0x9020	

start:
; BIOS 中断0x10 的读光标功能号 ah = 0x03
; 输入：bh = 页号
; 返回：ch = 扫描开始线，cl = 扫描结束线，
; dh = 行号(0x00 是顶端)，dl = 列号(0x00 是左边)。
	mov	ax,INITSEG		; 将ds 置成#INITSEG(0x9000)。这已经在bootsect 程序中设置过，但是现在是setup 程序，Linus 觉得需要再重新设置一下。
	mov	ds,ax
	mov	ah,0x03	
	xor	bh,bh
	int	0x10	
	mov	[0],dx			; 将光标位置信息存放在0x90000 处，控制台初始化时会来取。


; BIOS 中断0x15，功能号ah = 0x88
; 返回：ax = 从0x100000（1M）处开始的扩展内存大小(KB)。
; 若出错则CF 置位，ax = 出错码。
	mov	ah,0x88
	int	0x15
	mov	[2],ax			; 将扩展内存数值存在0x90002 处（1 个字）


; 取显示卡当前显示模式
; 调用BIOS 中断0x10，功能号 ah = 0x0f
; 返回：ah = 字符列数，al = 显示模式，bh = 当前显示页。
	mov	ah,0x0f
	int	0x10
	mov	[4],bx		; 0x90004(1 字)存放当前页
	mov	[6],ax		; 0x90006 显示模式,，0x90007 字符列数


; 检查显示方式（EGA/VGA）并取参数
; 调用BIOS 中断0x10
; 功能号：ah = 0x12，bl = 0x10
; 返回：bh = 显示状态
; (0x00 - 彩色模式，I/O 端口=0x3dX)
; (0x01 - 单色模式，I/O 端口=0x3bX)
; bl = 安装的显示内存
; (0x00 - 64k, 0x01 - 128k, 0x02 - 192k, 0x03 = 256k)
; cx = 显示卡特性参数
	mov	ah,0x12
	mov	bl,0x10
	int	0x10
	mov	[8],ax			; 0x90008 = ??
	mov	[10],bx			; 0x9000A = 安装的显示内存，0x9000B = 显示状态(彩色/单色)
	mov	[12],cx			; 0x9000C = 显示卡特性参数


; 取第一个硬盘的信息（复制硬盘参数表）
; 第1 个硬盘参数表的首地址竟然是中断向量0x41 的向量值！而第2 个硬盘
; 参数表紧接第1 个表的后面，中断向量0x46 的向量值也指向这第2 个硬盘
; 的参数表首址。表的长度是16 个字节(0x10)。
; 下面两段程序分别复制BIOS 有关两个硬盘的参数表，0x90080 处存放第1 个
; 硬盘的表，0x90090 处存放第2 个硬盘的表。
	mov	ax,0x0000
	mov	ds,ax
	lds	si,[4*0x41]		; 取中断向量0x41 的值，也即hd0 参数表的地址->ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0x0080		; 传输的目的地址: 0x9000:0x0080 -> es:di
	mov	cx,0x10			; 共传输0x10 字节
	rep
	movsb
; 取得第二个硬盘的信息
	mov	ax,0x0000
	mov	ds,ax
	lds	si,[4*0x46]		; 取中断向量0x46 的值，也即hd1 参数表的地址->ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0x0090
	mov	cx,0x10
	rep
	movsb


; 检查系统是否存在第2 个硬盘，如果不存在则第2 个表清零
; 利用BIOS 中断调用0x13 的取盘类型功能
; 功能号 ah = 0x15；
; 输入：dl = 驱动器号（0x8X 是硬盘：0x80 指第1 个硬盘，0x81 第2 个硬盘）
; 输出：ah = 类型码；00 --没有这个盘，CF 置位； 01 --是软驱，没有change-line 支持；
; 02 --是软驱(或其它可移动设备)，有change-line 支持； 03 --是硬盘。
	mov	ax,0x01500
	mov	dl,0x81
	int	0x13
	jc	no_disk1
	cmp	ah,3			; 是硬盘吗？（类型= 3 ？）
	je	is_disk1
no_disk1:
	mov	ax,INITSEG		; 第2 个硬盘不存在，则对第2 个硬盘表清零。
	mov	es,ax
	mov	di,0x0090
	mov	cx,0x10
	mov	ax,0x00
	rep
	stosb
is_disk1:

	mov	ah,0x03		
	xor	bh,bh
	int	0x10					; 读光标位置
	
	mov	cx,26
	mov	bx,0x0007	
	mov	bp,msg2
	mov ax,0x9020
	mov es,ax
	mov ax,0x1301	
	int	0x10					; 输出字符串

; 从这里开始我们要进入保护模式了

	cli			; 关中断

msg2:
	db 13,10
	db "Enter protected mode..."
	db 13,10,13,10

; 首先我们将system 模块移到正确的位置。
; bootsect 引导程序是将system 模块读入到从0x10000（64k）开始的位置。由于当时假设
; system 模块最大长度不会超过0x80000（512k），也即其末端不会超过内存地址0x90000，
; 所以bootsect 会将自己移动到0x90000 开始的地方，并把setup 加载到它的后面。
; 下面这段程序的用途是再把整个system 模块移动到0x00000 位置，即把从0x10000 到0x8ffff
; 的内存数据块(512k)，整块地向内存低端移动了0x10000（64k）的位置。
	mov	ax,0x0000
	cld		
do_move:
	mov	es,ax		; es:di 是目的地址(初始为0x0000:0x0)
	add	ax,0x1000	; 从0x1000 开始的64K开始移动
	cmp	ax,0x9000
	jz	end_move	; 已经把从0x8000 段开始的64k 代码移动完
	mov	ds,ax		
	sub	di,di
	sub	si,si
	mov cx,0x8000	; 移动 64K 字节
	rep
	movsw
	jmp	do_move

; 此后，我们加载段描述符
;
; 从这里开始会遇到32 位保护模式的操作，因此需要Intel 32 位保护模式编程方面的知识了,
; 有关这方面的信息请查阅列表后的简单介绍或附录中的详细说明。这里仅作概要说明。
;
; lidt 指令用于加载中断描述符表(idt)寄存器，它的操作数是6 个字节，0-1 字节是描述符表的
; 长度值(字节)；2-5 字节是描述符表的32 位线性基地址（首地址），其形式参见下面的说明。
; 中断描述符表中的每一个表项（8 字节）指出发生中断时
; 需要调用的代码的信息，与中断向量有些相似，但要包含更多的信息。
;
; lgdt 指令用于加载全局描述符表(gdt)寄存器，其操作数格式与lidt 指令的相同。全局描述符
; 表中的每个描述符项(8 字节)描述了保护模式下数据和代码段（块）的信息。其中包括段的
; 最大长度限制(16 位)、段的线性基址（32 位）、段的特权级、段是否在内存、读写许可以及
; 其它一些保护模式运行的标志.

end_move:
	mov	ax,SETUPSEG	
	mov	ds,ax				; ds 指向本程序(setup)段
	lidt	[idt_48]		; 加载中断描述符表(idt)寄存器，idt_48 是6 字节操作数的位置. 前2 字节表示idt 表的限长，后4 字节表示idt 表! 所处的基地址。
	lgdt	[gdt_48]		; 加载全局描述符表(gdt)寄存器，gdt_48 是6 字节操作数的位置


; 上面的操作完成后， 我们将开启A20 地址线
	call	 empty_8042		; 等待输入缓冲器空, 只有当输入缓冲器为空时才可以对其进行写命令
	mov	al,0xD1				; 0xD1 命令码-表示要写数据到8042 的P2 端口。
	out	0x64,al				; P2 端口的位1 用于A20 线的选通。数据要写到0x60 口。
	call 	empty_8042
	mov	al,0xDF				; 选通A20 地址线的参数
	out	0x60,al
	call	empty_8042		; 输入缓冲器为空，则表示A20 线已经选通


; 如果上面一切正常， 现在我们必须重新对中断进行编程
; 我们将它们放在正好处于intel 保留的硬件中断后面，在int 0x20-0x2F。
; 在那里它们不会引起冲突。不幸的是IBM 在原PC 机中搞糟了，以后也没有纠正过来。
; PC 机的bios 将中断放在了0x08-0x0f，这些中断也被用于内部硬件中断。
; 所以我们就必须重新对8259 中断控制器进行编程。
	mov	al,0x11		;  0x11 表示初始化命令开始，是ICW1 命令字，表示边沿触发、多片8259 级连、最后要发送ICW4 命令字
	out	0x20,al		
	dw	0x00eb,0x00eb		; 是jmp $+2, jmp $+2的机器码。 这两条指令的主要作用是延时，可以延时（14 - 20）个时钟周期
							; 而NOP 的时钟周期是3个，若要达到相同的效果，则须写（5-7）次.
	out	0xA0,al				; 再发送到8259A 从芯片
	dw	0x00eb,0x00eb
	mov	al,0x20	
	out	0x21,al				; 送主芯片ICW2 命令字，起始中断号，要送奇地址
	dw	0x00eb,0x00eb
	mov	al,0x28		
	out	0xA1,al				; 送从芯片ICW2 命令字，从芯片的起始中断号
	dw	0x00eb,0x00eb
	mov	al,0x04				; 送主芯片ICW3 命令字，主芯片的IR2 连从芯片INT
	out	0x21,al
	dw	0x00eb,0x00eb
	mov	al,0x02				
	out	0xA1,al				; 送从芯片ICW3 命令字，表示从芯片的INT 连到主芯! 片的IR2 引脚上
	dw	0x00eb,0x00eb
	mov	al,0x01		
	out	0x21,al				; 送主芯片ICW4 命令字。8086 模式；普通EOI 方式，需发送指令来复位。初始化结束，芯片就绪。
	dw	0x00eb,0x00eb
	out	0xA1,al
	dw	0x00eb,0x00eb
	mov	al,0xFF		
	out	0x21,al				; 屏蔽主芯片所有中断请求
	dw	0x00eb,0x00eb
	out	0xA1,al


; 这里设置进入32 位保护模式运行。首先加载机器状态字(lmsw - Load Machine Status Word)，也称
; 控制寄存器CR0，其比特位0 置1 将导致CPU 工作在保护模式。
	mov	ax,0x0001	; 保护模式比特位(PE)
	lmsw	ax		; 加载机器状态字
	jmp	8:0			; 跳转至cs 段8，偏移0 处 
; 我们已经将system 模块移动到0x00000 开始的地方，所以这里的偏移地址是0。这里的段
; 值的8 已经是保护模式下的段选择符了，用于选择描述符表和描述符表项以及所要求的特权级。
; 段选择符长度为16 位（2 字节）；位0-1 表示请求的特权级0-3，linux 操作系统只
; 用到两级：0 级（系统级）和3 级（用户级）；位2 用于选择全局描述符表(0)还是局部描
; 述符表(1)；位3-15 是描述符表项的索引，指出选择第几项描述符。所以段选择符
; 8(0b0000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项，该项指出
; 代码的基地址是0，因此这里的跳转指令就会去执行system 中的代码。



; 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法 - 如果这里死机，
; 则说明PC 机有问题，我们就没有办法再处理下去了。
; 只有当输入缓冲器为空时（状态寄存器位2 = 0）才可以对其进行写命令。
empty_8042:
	dw	0x00eb,0x00eb		; 延时操作
	in	al,0x64				; 读AT 键盘控制器状态寄存器
	test	al,2			; 测试位2，输入缓冲器满？
	jnz	empty_8042			
	ret
  

; 全局描述符表开始处。描述符表由多个8 字节长的描述符项组成。这里给出了3 个描述符项。第1 项无用，但须存在。第2 项是系统代码段
; 描述符，第3 项是系统数据段描述符。每个描述符的具体含义参见列表后说明。
gdt:
	dw	0,0,0,0		; 第1 个描述符，不用
; 这里在gdt 表中的偏移量为0x08，当加载代码段寄存器(段选择符)时，使用的是这个偏移值。
	dw	0x07FF		; 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0x0000		; base address=0
	dw	0x9A00		; 代码段为只读， 可执行
	dw	0x00C0		; 颗粒度为 4096,32 位模式
; 这里在gdt 表中的偏移量是0x10，当加载数据段寄存器(如ds 等)时，使用的是这个偏移值。
DATA_DESCRIPTOR:
	dw	0x07FF		; 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0x0000		; base address=0
	dw	0x9200		; 代码段为只读， 可执行
	dw	0x00C0		; 颗粒度为 4096,32 位模式
	
; 下面是加载中断描述符寄存器 idtr 的指令 lidt要求的6字节操作数。前2字节是gdt表的限长，
; 后4字节是idt 表的线性基地址。这里全局表长度设置为2KB（0x7ff即可），因为每8字节组成
; 一个段描述符项，所以表中共可以有256项。

idt_48:
	dw	0			; idt limit=0
	dw	0,0			; idt base=0L

gdt_48:
	dw	0x800		; gdt limit=2048, 256 GDT entries
	dw	512+gdt,0x9	; gdt base = 0X9xxxx
